package cn.demoncat.util.http;

import cn.demoncat.util.exception.ParamRuntimeException;
import cn.demoncat.util.exception.RestRuntimeException;
import cn.demoncat.util.http.SslUtil.SSLConfig;
import cn.demoncat.util.http.entity.*;
import cn.demoncat.util.io.CloseUtil;
import cn.demoncat.util.lang.MapUtil;
import cn.demoncat.util.lang.constant.StringConstant;
import cn.demoncat.util.web.CookieUtil;
import cn.demoncat.util.web.UrlUtil;
import cn.demoncat.util.web.constant.HttpConstant;
import kotlin.Pair;
import okhttp3.*;
import okio.BufferedSink;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;

/**
 * HTTP请求工具 - OkHttp3
 * 
 * @author 延晓磊
 *
 * @since 2019年10月12日
 */
public class HttpUtil {

	private static final Logger LOG = LoggerFactory.getLogger(HttpUtil.class);

	/**
	 * 默认连接超时时间（秒）
	 */
	private static final int CONNECT_TIMEOUT = 30;
	/**
	 * 默认读取超时时间（秒）
	 */
	private static final int READ_TIMEOUT = 60;
	/**
	 * 默认流写入超时时间（秒）
	 */
	private static final int WRITE_TIMEOUT = 60;

	/**
	 * 公共的OkHttp客户端：尽量使用单例，因为每个Client都有独立的线程池
	 */
	private static OkHttpClient CLIENT = null;

	/**
	 * 定制的OkHttp客户端
	 */
	private static final Map<String, OkHttpClient> CLIENTS = new HashMap<>();

	/**
	 * 同步请求
	 * 
	 * @param request	请求(支持重试)
	 * 
	 * @return 响应（状态码可能非200）
	 * 
	 * @throws RestRuntimeException 出现IOException
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月14日
	 */
	public static HttpResponse request(HttpRequest request) {
		LOG.info(HttpConstant.HTTP_SEND_LOG, request.getMethod(), request.getUrl());
		// 请求信息
		Request req = createRequest(request);
		// 请求客户端
		OkHttpClient client = getClient(request);
		// 请求器
		Call call = client.newCall(req);
		// 发起同步请求
		Response rep = null;
		if (request.getTimes() != null && request.getTimes() > 0) {
			// 重试
			for (int i = 1; i <= request.getTimes(); i++) {
				try {
					// 执行请求
					rep = call.execute();
					// 封装响应
					return getResponse(call, rep);
				} catch (Exception e) {
					if (i < request.getTimes()) {
						// 重试
						LOG.warn("请求失败，重新尝试：" + i, e);
						call = client.newCall(req);
					} else {
						// 最后一次：IOException
						throw RestRuntimeException.httpUrlError(request.getUrl(), e);
					}
				} finally {
					// 关闭响应
					CloseUtil.close(rep);
				}
			}
			// 请求未发送
			return HttpResponse.buildUnborn(request.getUrl());
		} else {
			// 直接请求
			try {
				// 执行请求
				rep = call.execute();
				// 封装响应
				return getResponse(call, rep);
			} catch (IOException e) {
				throw RestRuntimeException.httpUrlError(request.getUrl(), e);
			} finally {
				// 关闭响应
				CloseUtil.close(rep);
			}
		}
	}

	/**
	 * 异步请求（开启线程池发起请求）
	 * 
	 * @param request	请求（不支持重试）
	 * @param callback	回调
	 * 
	 * @throws RestRuntimeException 出现IOException
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月14日
	 */
	public static void request(HttpRequest request, HttpCallback callback) {
		LOG.info(HttpConstant.HTTP_SEND_LOG, request.getMethod(), request.getUrl());
		// 请求信息
		Request req = createRequest(request);
		// 请求客户端
		OkHttpClient client = getClient(request);
		// 请求器
		Call call = client.newCall(req);
		// 发起异步请求
		call.enqueue(callback.addRequest(request));
	}

	/**
	 * 获取响应结果
	 * 
	 * @param call		回调
	 * @param rep		响应
	 * @return
	 * 
	 * @author 延晓磊
	 * @throws IOException 
	 *
	 * @since 2019年10月14日
	 */
	public static HttpResponse getResponse(Call call, Response rep) throws IOException {
		HttpResponse response = new HttpResponse();
		// 获取响应体
		ResponseBody repBody = rep.body();
		// 成功响应
		if (rep.code() == 200) {
			// 响应内容
			response.setBody(repBody.bytes());
		} else {
			// 失败取消
			if (!call.isCanceled()) {
				call.cancel();
			}
		}
		// 响应类型
		if (repBody.contentType() != null) {
			response.setContentType(repBody.contentType().toString());
		}
		// 响应头
		Iterator<Pair<String, String>> iterator = rep.headers().iterator();
		Pair<String, String> header;
		Map<String, List<String>> headers = new HashMap<>();
		List<String> headerList;
		while (iterator.hasNext()) {
			header = iterator.next();
			headerList = headers.computeIfAbsent(header.getFirst(), k -> new ArrayList<>());
			headerList.add(header.getSecond());
		}
		response.setHeaders(headers);
		// 响应结果
		response.setMsg(rep.message());
		response.setStatus(rep.code());
		// 请求地址
		response.setUrl(call.request().url().toString());
		return response;
	}

	/**
	 * 创建请求体
	 * 
	 * @param request
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月14日
	 */
	private static RequestBody createRequestBody(HttpRequest request) {
		switch (request.getType()) {
			case FORM:
				// x-www-form-urlencoded
				FormBody.Builder formBuilder = new FormBody.Builder();
				// 添加参数
				if (MapUtil.isNotEmpty(request.getParams())) {
					for (Entry<String, String> entry : request.getParams().entrySet()) {
						formBuilder.add(entry.getKey(), entry.getValue());
					}
				}
				return formBuilder.build();
			case MULTIPART:
				// multipart/form-data
				MultipartBody.Builder multipartBuilder = new MultipartBody.Builder();
				multipartBuilder.setType(MultipartBody.FORM);
				// 添加参数
				if (MapUtil.isNotEmpty(request.getParams())) {
					for (Entry<String, String> entry : request.getParams().entrySet()) {
						multipartBuilder.addFormDataPart(entry.getKey(), entry.getValue());
					}
				}
				// 添加文件
				if (MapUtil.isNotEmpty(request.getFiles())) {
					for (Entry<String, List<HttpFileParam>> entry : request.getFiles().entrySet()) {
						for (HttpFileParam val : entry.getValue()) {
							multipartBuilder.addFormDataPart(entry.getKey(), val.getName(), new FileRequestBody(val));
						}
					}
				}
				return multipartBuilder.build();
			case JSON:
			case XML:
			case TEXT:
			case HTML:
				// 文本请求
				return RequestBody.create(request.getBody().toString(), MediaType.parse(request.getContentType()));
			case BINARY:
				// 二进制流请求
				return RequestBody.create((byte[]) request.getBody(), MediaType.parse(request.getContentType()));
			default:
				throw new ParamRuntimeException("请求类型错误：" + request.getType());
		}
	}

	/**
	 * 创建请求实例
	 * 
	 * @param request
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月14日
	 */
	private static Request createRequest(HttpRequest request) {
		// 请求构建器
		Request.Builder builder = new Request.Builder();
		// 组装URL和参数
		switch (request.getMethod()) {
		    case HttpConstant.Method.GET:
		    	// get
				builder.url(Objects.requireNonNull(HttpUrl.parse(UrlUtil.toUrl(request.getUrl(), request.getParams()))));
				builder.get();
		        break;
			case HttpConstant.Method.DELETE:
				// delete
				builder.url(Objects.requireNonNull(HttpUrl.parse(UrlUtil.toUrl(request.getUrl(), request.getParams()))));
				builder.delete();
				break;
		    default:
		    	// post/put
				if (request.getType().equals(HttpConstant.RequestType.FORM) || request.getType().equals(HttpConstant.RequestType.MULTIPART)) {
					// 表单参数
					builder.url(Objects.requireNonNull(HttpUrl.parse(request.getUrl())));
				}else{
					// 请求体参数
					builder.url(Objects.requireNonNull(HttpUrl.parse(UrlUtil.toUrl(request.getUrl(), request.getParams()))));
				}
				if (HttpConstant.Method.POST.equals(request.getMethod())) {
					builder.post(createRequestBody(request));
				}else{
					builder.put(createRequestBody(request));
				}
				break;
		}
		// 设置请求头
		if (MapUtil.isNotEmpty(request.getCookies())) {
			request.setHeaders(CookieUtil.addCookie(request.getHeaders(), request.getCookies()));
		}
		if (MapUtil.isNotEmpty(request.getHeaders())) {
			for (Entry<String, String> entry : request.getHeaders().entrySet()) {
				builder.header(entry.getKey(), entry.getValue());
			}
		}
		return builder.build();
	}

	/**
	 * 获取客户端：如果request指定超时和SSL，将创建新的OkHttpClient，否则返回默认的OkHttpClient
	 * 
	 * @param request
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月14日
	 */
	private static OkHttpClient getClient(HttpRequest request) {
		// 默认请求：没有设置超时、SSL
		if (request.getConnectTimeout() <= 0 && request.getReadTimeout() <= 0 && request.getWriteTimeout() <= 0 && request.getCertificate() == null) {
			return getDefaultClient();
		}
		// 创建OkHttp客户端
		return createClient(request.getConnectTimeout(), request.getReadTimeout(), request.getWriteTimeout(), request.getCertificate());
	}

	/**
	 * 获取客户端：默认的单例OkHttpClient
	 * 
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月14日
	 */
	private static OkHttpClient getDefaultClient() {
		if (CLIENT == null) {
			// 单例新建
			synchronized (HttpUtil.class) {
				if (CLIENT == null) {
					CLIENT = createClient(0, 0, 0, null);
				}
			}
		}
		return CLIENT;
	}

	/**
	 * 创建OkHttpClient
	 * 
	 * @param connectTimeout
	 * @param readTimeout
	 * @param writeTimeout
	 * @param cert
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月14日
	 */
	private static OkHttpClient createClient(int connectTimeout, int readTimeout, int writeTimeout,
			HttpSslCertificateParam cert) {
		// 缓存获取
		String key = connectTimeout +
				StringConstant.BAR +
				readTimeout +
				StringConstant.BAR +
				writeTimeout +
				StringConstant.BAR +
				(cert == null ? 0 : cert.hashCode());
		OkHttpClient client = CLIENTS.get(key);
		if (client == null) {
			// 创建构建器
			OkHttpClient.Builder builder = new OkHttpClient.Builder()
					// 主机验证规则
					.hostnameVerifier((hostname, session) -> true)
					// 连接超时
					.connectTimeout(connectTimeout <= 0 ? CONNECT_TIMEOUT : connectTimeout, TimeUnit.SECONDS)
					// 默认读取超时
					.readTimeout(readTimeout <= 0 ? READ_TIMEOUT : readTimeout, TimeUnit.SECONDS)
					// 默认写入超时
					.writeTimeout(writeTimeout <= 0 ? WRITE_TIMEOUT : writeTimeout, TimeUnit.SECONDS);
			// SSL证书
			if (cert != null) {
				SSLConfig ssl = SslUtil.parse(cert);
				builder.sslSocketFactory(ssl.getSslSocketFactory(), ssl.getX509TrustManager());
			}
			// 创建OkHttp客户端
			client = builder.build();
		}
		return client;
	}

	/**
	 * OkHttp Multipart请求的文件请求体
	 * 
	 * @author 延晓磊
	 *
	 * @since 2019年10月14日
	 */
	private static class FileRequestBody extends RequestBody {

		// 文件参数
		private final HttpFileParam file;

		public FileRequestBody(HttpFileParam file) {
			super();
			this.file = file;
		}

		@Override
		public MediaType contentType() {
			return MediaType.parse(file.getContentType());
		}

		@Override
		public void writeTo(BufferedSink sink) throws IOException {
			sink.write(this.file.getFile());
		}

		@Override
		public long contentLength() {
			return file.getFile().length;
		}

	}
}
